import { DocumentSymbol, Range, SymbolKind } from "vscode-languageserver";
import { t, util } from "./analysis";
import { syntaxFlagToDocumentSymbolKind } from "./translation";

type DocumentSymbolNode =
| t.Signature<t.SignatureKind>
| t.Declaration
| t.PPDefine
;

function isDocumentSymbolNode(node: t.Node): node is DocumentSymbolNode {
    return node.isDeclaration()
        || node.isSignature()
        || node.isPreprocessor<t.PPDefine>(t.SyntaxKind.ppDefine)
        ;
}

function isDocumentSymbolStopNode(node: t.Node): boolean {
    return node.isDeclaration<t.VariableDecl>(t.SyntaxKind.varDecl)
        || node.isDeclaration<t.ConstantDeclaration>(t.SyntaxKind.constDecl)
        || node.isSignature<t.ConstSignature>(t.SyntaxKind.constSignature)
        || node.isSignature<t.ParameterSignature>(t.SyntaxKind.parameterSignature)
        || node.isSignature<t.EnumMemberSignature>(t.SyntaxKind.enumMemberSignature)
        || node.isPreprocessor<t.PPDefine>(t.SyntaxKind.ppDefine)
        ;
}

function getRange(node: t.Node, source: t.SourceFile): Range {
    return Range.create(source.getLineAndCharactorOfPosition(node.start), source.getLineAndCharactorOfPosition(node.end));
}

function makeMetadataPropertyDocumentSymbol(property: t.ReadonlyTextRange & t.MDMKeyValuePair, source: t.SourceFile) {
    return DocumentSymbol.create(
        property.name.text,
        undefined,
        SymbolKind.Property,
        getRange(property, source),
        getRange(property.name, source)
    );
}

function makeMetadataPropertiesDocumentSymbol(properties: t.MDMProperties, source: t.SourceFile) {
    let symbols: DocumentSymbol[] = [];
    let children: DocumentSymbol[] = [];
    properties.forEachItem(item => {
        children.push(makeMetadataPropertyDocumentSymbol(item, source));
    });
    // Properties
    let range = getRange(properties, source);
    symbols.push(DocumentSymbol.create(
        `${properties.areaName ? `${properties.areaName.text}:` : ""}properties`,
        undefined,
        SymbolKind.Object,
        range,
        range,
        children
    ));
    // Area:
    properties.forEachObject(obj => {
        symbols.push(...makeMetadataPropertiesDocumentSymbol(obj, source));
    });
    return symbols;
}

function makeMetadataTemplatesDocumentSymbol(templates: t.MDMTemplates, source: t.SourceFile): DocumentSymbol[] {
    let symbols: DocumentSymbol[] = [];
    let children: DocumentSymbol[] = [];
    templates.forEachItem(item => makeMetadataPropertyDocumentSymbol(item, source));
    // Templates
    let totalRange = getRange(templates, source);
    let selectRange = templates.name.isMissingNode() ? totalRange : getRange(templates.name, source);
    symbols.push(DocumentSymbol.create(
        `${templates.areaName ? `${templates.areaName.text}:` : ""}templates`,
        undefined,
        SymbolKind.Object,
        totalRange,
        selectRange,
        children
    ));
    // Area:
    templates.forEachObject(obj => {
        symbols.push(...makeMetadataTemplatesDocumentSymbol(obj, source));
    });
    return symbols;
}

function makeMetadataDeclarationDocumentSymbol<T extends t.MDMDeclaration>(node: T, source: t.SourceFile, name: string, kind: SymbolKind, childrenGetter: () => DocumentSymbol[] | undefined) {
    let totalRange = getRange(node, source);
    return DocumentSymbol.create(
        node.name.isMissingNode() ? name : node.name.text,
        undefined,
        kind,
        totalRange,
        node.name.isMissingNode() ? totalRange : getRange(node.name, source),
        childrenGetter()
    );
}

function makeMetadataControlStyleDocumentSymbol(style: t.MDMControlStyle, source: t.SourceFile) {
    return makeMetadataDeclarationDocumentSymbol(style, source, "controlstyle", SymbolKind.Object, () => {
        let children: DocumentSymbol[] = [];
        // items
        if (style.readOnly) {
            children.push(makeMetadataPropertyDocumentSymbol(style.readOnly, source));
        }
        if (style.accelerator) {
            children.push(makeMetadataPropertyDocumentSymbol(style.accelerator, source));
        }
        if (style.type) {
            children.push(makeMetadataPropertyDocumentSymbol(style.type, source));
        }
        return children;
    });
}

function makeMetadataCellStyleDocumentSymbol(style: t.MDMCellStyle, source: t.SourceFile) {
    return makeMetadataDeclarationDocumentSymbol(style, source, "cellstyle", SymbolKind.Object, () => {
        let children: DocumentSymbol[] = [];
        let key: keyof t.MDMCellStyleProperties;
        for (key in (style as t.MDMCellStyleProperties)) {
            let prop = style[key];
            if (prop) {
                children.push(makeMetadataPropertyDocumentSymbol(prop, source));
            }
        }
        return children;
    });
}

function makeMetadataFontStyleDocumentSymbol(style: t.MDMFontStyle, source: t.SourceFile) {
    return makeMetadataDeclarationDocumentSymbol(style, source, "fontstyle", SymbolKind.Object, () => {
        let children: DocumentSymbol[] = [];
        let key: keyof t.MDMFontStyleProperties;
        for (key in (style as t.MDMFontStyleProperties)) {
            let prop = style[key];
            if (prop) {
                children.push(makeMetadataPropertyDocumentSymbol(prop, source));
            }
        }
        return children;
    });
}

function makeMetadataStyleDocumentSymbol(style: t.MDMStyle, source: t.SourceFile, name: string) {
    let symbols: DocumentSymbol[] = [];
    let children: DocumentSymbol[] = [];
    if (style.font) {
        children.push(makeMetadataFontStyleDocumentSymbol(style.font, source));
    }
    if (style.control) {
        children.push(makeMetadataControlStyleDocumentSymbol(style.control, source));
    }
    if (style.cell) {
        children.push(makeMetadataCellStyleDocumentSymbol(style.cell, source));
    }
    let key: keyof t.MDMStyleProperties;
    for (key in (style as t.MDMStyleProperties)) {
        let prop = style[key];
        if (prop) {
            children.push(makeMetadataPropertyDocumentSymbol(prop, source));
        }
    }
    let totalRange = getRange(style, source);
    symbols.push(DocumentSymbol.create(
        `${style.areaName ? `${style.areaName.text}:` : ""}${name}`,
        undefined,
        SymbolKind.Object,
        totalRange,
        style.name.isMissingNode() ? totalRange : getRange(style.name, source),
        children
    ));
    // area
    style.forEachObject(obj => children.push(...makeMetadataStyleDocumentSymbol(obj, source, name)));
    return symbols;
}

function makeMetadataLabeledObjectDocumentSymbol<T extends t.MDMLabeledObject>(obj: T, source: t.SourceFile, getter: (node: T, source: t.SourceFile) => DocumentSymbol, childrenGetter: (node: T, source: t.SourceFile) => DocumentSymbol[]) {
    let symbol = getter(obj, source);
    let children: DocumentSymbol[] = [];
    if (obj.properties) {
        children.push(...makeMetadataPropertiesDocumentSymbol(obj.properties, source));
    }
    if (obj.templates) {
        children.push(...makeMetadataTemplatesDocumentSymbol(obj.templates, source));
    }
    if (obj.style) {
        children.push(...makeMetadataStyleDocumentSymbol(obj.style, source, "style"));
    }
    if (obj.labelStyle) {
        children.push(...makeMetadataStyleDocumentSymbol(obj.labelStyle, source, "labelstyle"));
    }
    children.push(...childrenGetter(obj, source));
    symbol.children = children;
    return symbol;
}

function makeMetadataCategoryDocumentSymbol(category: t.MDMCategory, source: t.SourceFile) {
    return makeMetadataLabeledObjectDocumentSymbol(category, source,
        (node, source) => {
            return DocumentSymbol.create(
                node.name.text,
                undefined,
                SymbolKind.EnumMember,
                getRange(node, source),
                getRange(node.name, source),
            );
        },
        (node, source) => {
            let children: DocumentSymbol[] = [];
            if (node.categories) {
                node.categories.forEachElement(ele => {
                    children.push(makeMetadataCategoryDocumentSymbol(ele, source));
                });
            }
            return children;
        }
    );
}

function makeMetadataClassDocumentSymbol(classObj: t.MDMClass, source: t.SourceFile) {
    return makeMetadataLabeledObjectDocumentSymbol(classObj, source,
        (node, source) => {
            let range = getRange(node, source);
            return DocumentSymbol.create(
                "@class",
                undefined,
                SymbolKind.Object,
                range,
                range,
            );
        },
        (node, source) => {
            let children: DocumentSymbol[] = [];
            if (node.types) {
                for (const type of node.types) {
                    children.push(makeMetadataFieldDocumentSymbol(type, source));
                }
            }
            if (node.fields) {
                for (const field of node.fields) {
                    children.push(makeMetadataFieldDocumentSymbol(field, source));
                }
            }
            if (node.pages) {
                for (const page of node.pages) {
                    children.push(makeMetadataFieldDocumentSymbol(page, source));
                }
            }
            return children;
        }
    );
}

function makeMetadataFieldDocumentSymbol(field: t.MDMField, source: t.SourceFile) {
    return makeMetadataLabeledObjectDocumentSymbol(field, source,
        (node, source) => {
            return DocumentSymbol.create(
                node.name.text,
                undefined,
                SymbolKind.Object,
                getRange(node, source),
                getRange(node.name, source),
            );
        },
        (node, source) => {
            let children: DocumentSymbol[] = [];
            if (node.categories) {
                node.categories.forEachElement(ele => {
                    children.push(makeMetadataCategoryDocumentSymbol(ele, source));
                });
            }
            if (node.class) {
                children.push(makeMetadataClassDocumentSymbol(node.class, source));
            }
            return children;
        }
    );
}

function makeMetadataHierarchicalView(node: t.MDMHierarchicalView, source: t.SourceFile) {
    return makeMetadataLabeledObjectDocumentSymbol(node, source,
        (node, source) => {
            return DocumentSymbol.create(
                "HDATA",
                undefined,
                SymbolKind.Object,
                getRange(node, source),
                getRange(node.name, source),
            );
        },
        () => {
            return [];
        },
    );
}

function getDocumentSymbolName(node: t.Declaration | t.Signature<t.SignatureKind> | t.PPDefine) {
    if (node.isDeclaration<t.EventSection>(t.SyntaxKind.eventSection)) {
        return `Event(${node.name.text})`;
    }
    return node.name.text;
}

function makeDocumentSymbolsFromArray(array: readonly t.Node[], source: t.SourceFile): DocumentSymbol[] {
    let symbols: DocumentSymbol[] = [];
    for (const item of array) {
        let nodeSymbol = makeDocumentSymbol(item, source);
        if (Array.isArray(nodeSymbol)) {
            symbols.push(...nodeSymbol);
        }
        else if (nodeSymbol) {
            symbols.push(nodeSymbol);
        }
    }
    return symbols;
}

function makeDocumentSymbol(arg: t.Node | t.NodeArray<t.Node>, source: t.SourceFile): DocumentSymbol | DocumentSymbol[] | undefined {

    if (util.isNodeArray(arg)) {
        return makeDocumentSymbolsFromArray(arg, source);
    }

    if (arg.isMissingNode()) {
        return;
    }

    if (arg.isStatement<t.Block>(t.SyntaxKind.block)) {
        return makeDocumentSymbolsFromArray(arg.statements, source);
    }

    if (arg.is<t.LocalDecls>(t.SyntaxKind.localDecls)) {
        let symbols: DocumentSymbol[] = [];
        for (const item of arg.declarations) {
            symbols.push(makeDocumentSymbol(item, source) as DocumentSymbol);
        }
        return symbols;
    }

    if (arg.is<t.MDMDocument>(t.SyntaxKind.mdmDocument)) {
        let symbols: DocumentSymbol[] = [];
        if (arg.hdata) {
            symbols.push(makeMetadataHierarchicalView(arg.hdata, source));
        }
        for (const types of arg.types) {
            symbols.push(makeMetadataFieldDocumentSymbol(types, source));
        }
        for (const field of arg.fields) {
            symbols.push(makeMetadataFieldDocumentSymbol(field, source));
        }
        return symbols;
    }

    if (!isDocumentSymbolNode(arg)) {
        return;
    }

    let range = getRange(arg, source);
    let children: DocumentSymbol[] = [];

    if (!isDocumentSymbolStopNode(arg)) {
        arg.forEachChild(
            node => {
                let symbol = makeDocumentSymbol(node, source);
                if (symbol) {
                    if (Array.isArray(symbol)) {
                        children.push(...symbol);
                    }
                    else {
                        children.push(symbol);
                    }
                }
            },
            nodeArray => {
                for (const n of nodeArray) {
                    let symbol = makeDocumentSymbol(n, source);
                    if (symbol) {
                        if (Array.isArray(symbol)) {
                            children.push(...symbol);
                        }
                        else {
                            children.push(symbol);
                        }
                    }
                }
            }
        );
    }

    return DocumentSymbol.create(getDocumentSymbolName(arg), undefined, syntaxFlagToDocumentSymbolKind(arg.kind), range, getRange(arg.name, source), children);
}

export async function provideDocumentSymbol(source: t.SourceFile): Promise<DocumentSymbol[] | undefined> {
    let symbols: DocumentSymbol[] = [];

    source.forEachChild(
        node => {
            let symbol = makeDocumentSymbol(node, source);
            if (symbol) {
                if (Array.isArray(symbol)) {
                    symbols.push(...symbol);
                }
                else {
                    symbols.push(symbol);
                }
            }
        },
        nodeArray => {
            for (const node of nodeArray) {
                let symbol = makeDocumentSymbol(node, source);
                if (symbol) {
                    if (Array.isArray(symbol)) {
                        symbols.push(...symbol);
                    }
                    else {
                        symbols.push(symbol);
                    }
                }
            }
        }
    );

    return symbols.length > 0 ? symbols : undefined;
}
